#!/usr/local/bin/python
################################################################################
# sender.py
#
# UTF8SMTP NIDAMail Program
# Version 1.0
# Date : July 23, 2008
# Homepage: http://sourceforge.net/projects/utf8smtp/
################################################################################

# Import 3rd party PyDNS library (http://pydns.sourceforge.net)
import DNS
import threading, time, os
from distutils import file_util
from socket import *
### STARTTLS				  
from tlslite.api import *
### STARTTLS				  

import common

vh = common.vh
conf = common.conf
access = common.access

################################################################################
def returnMail(mail, rcpt_to, subject, rfc822, errcode, contents):

	# Process flow: ENCODING/before -> encoder -> sender -> SEND MAIL

	# If the value of RETRY is 0 (The SMTP server failes to send an email.)
	if mail['RETRY'] < 1:
		common.printLOG('Fail  |  From: %s, To: %s, Subject: %s : %s' % (mail['MAIL_FROM'], rcpt_to, subject, contents.strip()), 'sender')

		# Send a return mail to notify an error
		if mail['MAIL_FROM'] != '%s@%s' % (conf['RETURN_ID'], conf['SERVER_NAME']):
			try:
	 			key19 = common.makeKEY19(0)

	 			# /NIDAMail_Data/ENCODING/before/key19 
				# Header
				f = open('%s/before/%s' % (conf['ENCODE_QUEUE'], key19), 'w')
				f.write('MAIL_FROM=%s@%s\r\n' % (conf['RETURN_ID'], conf['SERVER_NAME']))
				f.write('FROM_NAME=ReturnMail\r\n')
				f.write('FROM_EMAIL=%s@%s\r\n' % (conf['RETURN_ID'], conf['SERVER_NAME']))
				f.write('TO=%s\r\n' % mail['MAIL_FROM'])
				f.write('CHARSET=utf-8\r\n')
				f.write('SUBJECT=%s\r\n' % (subject % rcpt_to))
				f.write('MAINTEXT=%s_maintext\r\n' % key19)
				f.write('RFC822=%s_rfc822\r\n' % key19)
				f.write('X-ERRCODE=%s\r\n' % errcode)
				f.close()
				
				# maintext (Contents)
	 			# /NIDAMail_Data/ENCODING/original/key19_maintext 
				f = open('%s/original/%s_maintext' % (conf['ENCODE_QUEUE'], key19), 'w')
				f.write('response: %s' % contents)
				f.close()

				# Attach a failed email message in RFC822 format
	 			# /NIDAMail_Data/SEND/original/rfc822 -> /NIDAMail_Data/SEND/original/key19_rfc822
				file_util.copy_file('%s/original/%s' % (conf['SEND_QUEUE'], rfc822), '%s/original/%s_rfc822' % (conf['ENCODE_QUEUE'], key19))

				# 
	 			# /NIDAMail_Data/ENCODING/before/key19 -> /NIDAMail_Data/ENCODING/key19
				os.rename('%s/before/%s' % (conf['ENCODE_QUEUE'], key19), '%s/%s' % (conf['ENCODE_QUEUE'], key19))
			except: 
				common.printLOG("sender  | Can't returnMail(%s, %s, -, -, %s)" % (mail['MAIL_FROM'], rcpt_to, errcode), 'error')
	else:
		# Decrease the number of RETRY by 1
		mail['RETRY'] -= 1
		print "Retry: " + mail['RETRY']

		# common.py - SENDER_RETRY_INTERVAL = 10*60
		key19 = common.makeKEY19(time.time()+conf['SENDER_RETRY_INTERVAL'])

		try:
 			# /NIDAMail_Data/SEND/before/key19
			f = open('%s/before/%s' % (conf['SEND_QUEUE'], key19), 'w')
			f.write('MAIL_FROM=%s\r\n' % mail['MAIL_FROM'])

			for email in mail['TO']: 
				f.write('TO=%s\r\n' % email)

			f.write('RETRY=%d\r\n' % mail['RETRY'])
			f.close()

 			# /NIDAMail_Data/SEND/original/rfc822 -> /NIDAMail_Data/SEND/original/key19_original
			file_util.copy_file('%s/original/%s' % (conf['SEND_QUEUE'], rfc822), '%s/original/%s_original' % (conf['SEND_QUEUE'], key19))

 			# /NIDAMail_Data/SEND/before/key19 -> /NIDAMail_Data/SEND/key19
			os.rename('%s/before/%s' % (conf['SEND_QUEUE'], key19), '%s/%s' % (conf['SEND_QUEUE'], key19))
		except: 
			common.printLOG("sender | Can't retryMail(%s, %s, -, -, %s)" % (mail['MAIL_FROM'], rcpt_to, errcode), 'error')
		else: 
			common.printLOG('Retry | From: %s, To: %s, Subject: %s, Retry: %d'% (mail['MAIL_FROM'], rcpt_to, subject, mail['RETRY']), 'sender')
################################################################################
def recvLINE(conn):
	lineStr = ''
	while 1:
		try:
			recvStr = conn.recv(1024)
			if recvStr == '':
				common.printLOG('Disconnected from client', 'sender')
				break
		except:
			common.printLOG('Timeout', 'sender')
			break
		else:
		  lineStr = lineStr + recvStr
		  if lineStr.find('\n') != -1:
		  	break
	return lineStr
################################################################################
def sendESMTP(file):

	# EAI Implementation
	#mail = {'MAIL_FROM':'', 'TO':[], 'RETRY': conf['SENDER_RETRY_COUNT'], 'STATE':''}
	mail = {'MAIL_FROM':'', 'MAIL_FROM_ALT':'', 'TO':[], 'TO_ALT':'', 'RETRY': conf['SENDER_RETRY_COUNT'], 'STATE':'' }

	#---------------------------------------------------------------------------
	# /NIDAMail_Data/SEND/ing/file
	#---------------------------------------------------------------------------
	# Open a file to send an email
	try: 
		f = open('%s/ing/%s' % (conf['SEND_QUEUE'], file), 'r')
	except: 
		common.printLOG("sender | Can't open %s/ing/%s" % (conf['SEND_QUEUE'], file), 'error')

	else:
		for lineStr in f:
			lineStr = lineStr.strip()
			cmdStr = lineStr.upper()
			#-------------------------------------------------------------------
			# MAIL FROM
			#-------------------------------------------------------------------
			if cmdStr.startswith('MAIL_FROM='):
				szTemp = lineStr[10:].strip().lower()
				if common.search_access(szTemp, ['from', 'both'])[2] == 'deny':
					common.printLOG('Access denied from %s by %s.access' % (szTemp, common.PROGRAM_NAME), 'sender')
					mail['TO'] = []
					break
				else: 
					mail['MAIL_FROM'] = szTemp
			####################################################################
			# EAI Implementation
			# MAIL_FROM_ALT			
			####################################################################
			elif cmdStr.startswith('MAIL_FROM_ALT='):
				szTemp = lineStr[14:].strip().lower()

				if common.search_access(szTemp, ['from', 'both'])[2] == 'deny':
					common.printLOG('Access denied from %s by %s.access' % (szTemp, common.PROGRAM_NAME), 'sender')
					mail['TO'] = []
					break
				else: 
					mail['MAIL_FROM_ALT'] = szTemp
			#-------------------------------------------------------------------
			# TO
			#-------------------------------------------------------------------
			elif cmdStr.startswith('TO='):
				szTemp = lineStr[3:].strip().lower()
				if common.search_access(szTemp, ['to', 'both'])[2] == 'deny': 
					common.printLOG('Access denied to %s by %s.access' % (szTemp, common.PROGRAM_NAME), 'sender')
				else: 
					mail['TO'].append(szTemp)
			###################################################################################
			# EAI Implementation
			# TO ALT
			###################################################################################
			elif cmdStr.startswith('TO_ALT='):
				szTemp = lineStr[7:].strip().lower()
				#if common.search_access(szTemp, ['to', 'both'])[2] == 'deny': 
				#	common.printLOG('Access denied to %s by %s.access' % (szTemp, common.PROGRAM_NAME), 'sender')
				#else: 
				mail['TO_ALT'] = szTemp
			#-------------------------------------------------------------------
			# RETRY
			#-------------------------------------------------------------------
			elif cmdStr.startswith('RETRY='): 
				mail['RETRY'] = int(lineStr[6:])
		f.close()
	#---------------------------------------------------------------------------
	# TO
	#---------------------------------------------------------------------------
	for rcpt_to in mail['TO']:
	
		mail['STATE'] = ''				
	
		rcpt_to = common.makeEMAIL(rcpt_to)
	
		# Split domain name part
		domain  = rcpt_to.split('@')[1]
	
		if conf['IDN_USE'] == 1:
			# Punycode convert
			domain = domain.decode('utf-8', 'ignore').encode('idna')
		#-----------------------------------------------------------------------
		# Query MX resource record of domain
		# Use 3rd party PyDNS library (http://pydns.sourceforge.net)
		#-----------------------------------------------------------------------
		i = 0
		while 1:
			i = i + 1
			try:
				# Create an execution utility function
				do_it = lambda x:x['data']
				# let DNS.Request(domain, qtype='MX').req().answers be functions that perform actions
				# map(function, iterable, ...) - Apply function to every item of iterable and return a list of the results.
				# map() is used to convert a list. 
				# The given function is called for every element in the original list, 
				# and a new list is created which contains the return values from the lambda function. 
				answers = map(do_it, DNS.Request(domain, qtype='MX').req().answers)

				# print "MX: " + answers
				answers.append((10, domain))

			except:
				common.printError("sender | Can't get mx records from %s (%d)" % (rcpt_to, i), 'sender')

				if i < 4: 
					time.sleep(1)
					pass
				else: 
					answers = [(10, domain)]
					mail['STATE'] = 'mx fail'
					break
			else: 
				break

		#-----------------------------------------------------------------------
		# Connection to MX
		#-----------------------------------------------------------------------
		for mx, rdata in answers:
	
			conn = socket(AF_INET, SOCK_STREAM)
			conn.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
			conn.settimeout(conf['SENDER_TIMEOUT'])
	
			try:
				# rdata = MX record
				conn.connect((rdata, conf['SENDER_PORT']))
			except:
				if mail['STATE'] != 'mx fail': 
					mail['STATE'] = 'connection fail'
				conn.close()
				continue
  	
  			try:
				mail['STATE'] = ''
				szTemp = recvLINE(conn)
				#---------------------------------------------------------------
				# HELO
				#---------------------------------------------------------------
				#if szTemp.startswith('220'): 
				#	conn.sendall('HELO %s\r\n' % (conf['SERVER_NAME']))
				#else:
				#	mail['STATE'] = 'error'
				#	returnMail(mail, rcpt_to, 'send failed(%s) - FIRST', '%s_original' % (file), '3', szTemp)
				#	conn.close()
				#	break

				#---------------------------------------------------------------
				# EHLO
				#---------------------------------------------------------------
				#print szTemp
				if szTemp.startswith('220'): 
					conn.sendall('EHLO %s\r\n' % (conf['SERVER_NAME']))
				else:
					mail['STATE'] = 'error'
					returnMail(mail, rcpt_to, 'send failed(%s) - FIRST', '%s_original' % (file), '3', szTemp)
					conn.close()
					break
				#---------------------------------------------------------------
				szTemp = recvLINE(conn)
				#---------------------------------------------------------------
				if szTemp.startswith('250'):
					#-------------------------------------------------------------
					# Check if the SMTP supports UTF8SMTP
					#-------------------------------------------------------------
					if (szTemp.upper().find('UTF8SMTP') == -1):
						print "No UTF8SMTP Support"
						UTF8SMTP = 0
					else:
						print "UTF8SMTP Support"
						UTF8SMTP = 1
					#-------------------------------------------------------------
					# STARTTLS
					#-------------------------------------------------------------
					print szTemp
					if szTemp.upper().find('STARTTLS') != -1:
						print "STARTTLS on"
						conn.sendall('STARTTLS\r\n')
						szTemp = recvLINE(conn)
						print szTemp

						if szTemp.startswith('220'):
							try:
								connTLS = TLSConnection(conn)
								connTLS.handshakeClientCert()
							except:
								common.printLOG("sender | Can't STARTTLS", 'error')
								mail['STATE'] = 'error'
								returnMail(mail, rcpt_to, 'send failed(%s) - STARTTLS', '%s_original' % (file), '3', szTemp)
								conn.close()
								break
							else:
								conn = connTLS
         			               
						conn.sendall('EHLO %s\r\n' % (conf['SERVER_NAME']))
						#-----------------------------------------------------------
						szTemp = recvLINE(conn)
						#-----------------------------------------------------------
				#---------------------------------------------------------------
				if not szTemp.startswith('250'):
					sendallForTLS(conn, 'HELO %s\r\n' % (conf['SERVER_NAME']))
					szTemp = recvLINE(conn)
				#---------------------------------------------------------------
				if szTemp.startswith('250'):
					if conf['IDN_USE'] == 1:
						# UTF8SMTP Support
						if UTF8SMTP == 1:
							print "1: mail from: " + mail['MAIL_FROM']
							conn.sendall('MAIL FROM: <%s>\r\n' % mail['MAIL_FROM'].decode('utf-8', 'ignore').encode(conf['IDN_CHARSET'], 'ignore'))
						# No UTF8SMTP Support
						else:
							print "2-1: mail from: " + str(mail['MAIL_FROM'])
							print "2-2: mail from alt: " + str(mail['MAIL_FROM_ALT'])

							if mail['MAIL_FROM_ALT'] != '':
								print "2-3: There is ALT"
								conn.sendall('MAIL FROM: <%s>\r\n' % mail['MAIL_FROM_ALT'].decode('utf-8', 'ignore').encode(conf['IDN_CHARSET'], 'ignore'))
							else:
								print "2-4: No Alt"
								conn.sendall('MAIL FROM: <%s>\r\n' % mail['MAIL_FROM'].decode('utf-8', 'ignore').encode(conf['IDN_CHARSET'], 'ignore'))
					else: 
						conn.sendall('MAIL FROM: <%s>\r\n' % mail['MAIL_FROM'])
				else:
					mail['STATE'] = 'error'				
					returnMail(mail, rcpt_to, 'send failed(%s) - HELO', '%s_original' % (file), '3', szTemp)
					conn.close()
					break
				#---------------------------------------------------------------
				szTemp = recvLINE(conn)
				#if szTemp.find('Sender ok') != -1:
				if szTemp.startswith('250'):
					############################################################################
					# EAI Implementation
					############################################################################
					if conf['IDN_USE'] == 1:
						# UTF8SMTP Support
						if UTF8SMTP == 1:
							print "3"
							print "rcpt_to: " + rcpt_to
							conn.sendall('RCPT TO: <%s>\r\n' % rcpt_to.decode('utf-8', 'ignore').encode(conf['IDN_CHARSET'], 'ignore'))
						# No UTF8SMTP Support
						else:
							if mail['TO_ALT'] != '':
								print "4-1 RCPT TO ALT: " + mail['TO_ALT']
								conn.sendall('RCPT TO: <%s>\r\n' % mail['TO_ALT'].decode('utf-8', 'ignore').encode(conf['IDN_CHARSET'], 'ignore'))
							else:
								print "4-2 RCPT TO: " + rcpt_to
								conn.sendall('RCPT TO: <%s>\r\n' % rcpt_to.decode('utf-8', 'ignore').encode(conf['IDN_CHARSET'], 'ignore'))
					else: 
						print "5: rcpt_to: " + rcpt_to
						conn.sendall('RCPT TO: <%s>\r\n' % (rcpt_to))
				else:
					mail['STATE'] = 'error'
					returnMail(mail, rcpt_to, 'send failed(%s) - MAIL FROM', '%s_original' % (file), '4', szTemp)
					conn.close()
					break
				#---------------------------------------------------------------
				szTemp = recvLINE(conn)
				print szTemp

				if szTemp.startswith('250'):
					conn.sendall('DATA\r\n')
				else:
					mail['STATE'] = 'error'				
					returnMail(mail, rcpt_to, 'send failed(%s) - RCPT TO', '%s_original' % (file), '5', szTemp)
					conn.close()
					break

				#---------------------------------------------------------------
				szTemp = recvLINE(conn)
				print szTemp
				# Send the message of original file
				if szTemp.startswith('354'):
					######################################################################
					# UTF8SMTP Support
					######################################################################
					if UTF8SMTP == 0:
						print "No"
						f = open('%s/original/%s_original_alt' % (conf['SEND_QUEUE'], file), 'r')
					else:
						print "Yes"
						f = open('%s/original/%s_original' % (conf['SEND_QUEUE'], file), 'r')
					#f = open('%s/original/%s_original' % (conf['SEND_QUEUE'], file), 'r')
					conn.sendall(f.read())
					f.close()
					conn.sendall('\r\n.\r\n')
				else:
					mail['STATE'] = 'error'				
					returnMail(mail, rcpt_to, 'send failed(%s) - DATA', '%s_original' % (file), '6', szTemp)
					conn.close()
					break
				#---------------------------------------------------------------
				szTemp = recvLINE(conn)
				if szTemp.startswith('250'): conn.sendall('QUIT\r\n')
				else:
					mail['STATE'] = 'error'				
					returnMail(mail, rcpt_to, 'send failed(%s) - QUIT', '%s_original' % (file), '6', szTemp)
					conn.close()
					break

				szTemp = recvLINE(conn)
				conn.close()
				break
			except: common.printError("sender  | Can't send mail %s" % file, 'sender')
		#-----------------------------------------------------------------------
		if mail['STATE'] == 'connection fail': 
			returnMail(mail, rcpt_to, 'send failed(%s) - Connection Failed', '%s_original' % (file), '7', 'Connection failed')
		elif mail['STATE'] == 'mx fail': 
			returnMail(mail, rcpt_to, 'send failed(%s) - DNS Failed', '%s_original' % (file), '1', 'DNS resolving failed')
		#-----------------------------------------------------------------------
		if mail['STATE'] == '': 
			common.printLOG('Success | From: %s, To: %s' % (mail['MAIL_FROM'], rcpt_to), 'sender')

	try: 
		os.unlink('%s/original/%s_original' % (conf['SEND_QUEUE'], file))
	except: 
		common.printLOG("sender  | Can't unlink %s/original/%s_original" % (conf['SEND_QUEUE'], file), 'error')

	try: 
		os.unlink('%s/ing/%s' % (conf['SEND_QUEUE'], file))
	except: 
		common.printLOG("sender  | Can't unlink %s/ing/%s" % (conf['SEND_QUEUE'], file), 'error')

################################################################################
# MAIN
################################################################################
DNS.DiscoverNameServers()

common.setUIDGID('sender')
common.printLOG('sender | SENDER is started', 'common')

# /NIDAMail_Data/SEND/before/
list = os.listdir('%s/before' % conf['SEND_QUEUE'])

for file in list:
	try: os.unlink('%s/before/%s' % (conf['SEND_QUEUE'], file))
	except: common.printLOG("sender | Can't unlink %s/before/%s" % (conf['SEND_QUEUE'], file), 'error')

# /NIDAMail_Data/SEND/ing/
list = os.listdir('%s/ing' % (conf['SEND_QUEUE']))

for file in list:
	# /NIDAMail_Data/SEND/ing/file ->  /NIDAMail_Data/SEND/
	try: os.rename('%s/ing/%s' % (conf['SEND_QUEUE'], file), '%s/%s' % (conf['SEND_QUEUE'], file))
	except: common.printLOG("sender  | Can't rename %s/ing/%s to %s/%s" % (conf['SEND_QUEUE'], file, conf['SEND_QUEUE'], file), 'error')

while 1:
	common.loadConf('sender')
	common.loadVH('sender')
	common.loadAccess('sender')

	# /NIDAMail_Data/SEND/
	list = os.listdir(conf['SEND_QUEUE'])

	for file in list:
		intNow = int(common.makeKEY19(0)[0:14])

		if not file in ('ing', 'before', 'original'):

			if int(file[0:14]) > intNow: continue

			try: os.rename('%s/%s' % (conf['SEND_QUEUE'], file), '%s/ing/%s' % (conf['SEND_QUEUE'], file))
			except: common.printLOG("sender | Can't rename %s/%s to %s/ing/%s" % (conf['SEND_QUEUE'], file, conf['SEND_QUEUE'], file), 'error')
			else:
				if conf['SENDER_TYPE'] == 'fork':
					pid = os.fork()
					if pid == 0:
						sendESMTP(file)
						os.sys.exit(0)
					th = threading.Thread(target=common.waitPID, args=())
					th.start()
					while(threading.activeCount() > conf['SENDER_MAX_THREADS']):
						common.printLOG('sender | fork count is larger than %s' % conf['SENDER_MAX_THREADS'], 'error')
						time.sleep(1)
				else:
  	  				th = threading.Thread(target=sendESMTP, args=(file, ))
					th.start()
					while(threading.activeCount() > conf['SENDER_MAX_THREADS']):
						common.printLOG('sender | thread count is larger than %s' % (conf['SENDER_MAX_THREADS']), 'error')
						time.sleep(1)
	time.sleep(1)
################################################################################
# End of file	
################################################################################
